<?php
/**
 * XGPHP 轻量级PHP框架
 * @link http:// xgphp.xg3.cn
 * @version 2.0.0
 * @license http:// www.apache.org/licenses/LICENSE-2.0R
 * @author 讯高科技 <xungaokeji@qq.com>
*/
namespace xg;
use \xg\facade\Route;
use \xg\facade\Config;
use \xg\facade\Lang;
use \helper\Arr;
use \helper\File;

class View {
	protected $config=[];
	protected $data=[];
	protected $route=[];
	protected $parser=[];

	public function __construct($config=null){
		if(!is_array($config)){
			$config = Config::get('view');
		}
		$this->config($config);
		$this->route=Route::data();
	}
	public function config($conf,$value=null){
		if(is_array($conf)){
			$this->config=Arr::extend($this->config,$conf);
		}elseif($value){
			$this->config[$conf]=$value;
		}
		if(!$this->config['suffix']){
			$this->config['suffix']='html';
		}
		
		if(!$this->config['path']){
			if(Route::data()){
				$this->config['path'] = File::path('xg:app','view');
			}else{
				unset($this->config['path']);
			}
		}else{
			$this->config['path'] = File::path($this->config['path']);
		}
		if($this->config['temp']){
			$this->config['temp'] = File::path($this->config['temp']);
		}else{
			$this->config['temp'] = File::path(realpath(sys_get_temp_dir()),'xgphp');
		}
		
		if(is_null($value) and is_string($conf)){
			return $this->config[$conf];
		}
		return $this;
	}
	public function data(){
		return $this->data;
	}
	// 渲染模板
	public function fetch($tpl='',$xg_template_data=[]) {
		Hook::create('view.fetch-before')->run($tpl,$xg_template_data,$this);
		$xg_template_data=Arr::extend($this->data,$xg_template_data);
		extract($xg_template_data,EXTR_OVERWRITE);
		if(!$tpl and $this->route) {
			$tpl = $this->route['action'];
		}
		ob_start();
		require $this->phpFile($tpl);
		$cont = ob_get_contents();
		ob_end_clean();
		Hook::create('view.fetch-after')->run($tpl,$xg_template_data,$this);
		return $cont;
	}
	// 获取模板路径 分割符号 ~插件 @应用 #主题
	public function tplPath(string $tpl) {
		// 备份一下原模板变量
		$_tpl=$tpl;
		// 添加模板扩展名
		$tpl=File::addExt($tpl,$this->config['suffix']);
		
		// 如果存在此模板文件
		if(is_file($tpl)){
			return $tpl;
		}

		// 开始解析特殊模板
		if(($pos=strpos($tpl,'~'))!==false) // 插件模板 名称里带有~字符
		{
			$addon=substr($tpl,0,$pos);
			if($addon){
				// 限制只能读取插件内部文件
				$dir=File::path('xg:addons',$addon);
			}
			$tpl=substr($tpl,$pos+1);
		}
		elseif(($pos=strpos($tpl,'@'))!==false) // 应用模板 名称里带有@字符
		{
			$app=substr($tpl,0,$pos);
			if(!$app){
				if($this->route['app']){
					$app=$this->route['app'];
				}
			}
			$dir=File::path('xg:app',$app);
			$tpl=substr($tpl,$pos+1);
		}else{
			$dir=$this->config['path'];
		}
		
		if(($pos=strpos($tpl,'#'))!==false) // 是否在模板变量中指定了主题
		{
			if($pos>0){
				$theme=substr($tpl,0,$pos);
			}elseif($this->config['theme']){
				$theme=$this->config['theme'];
			}else{
				// 没有设置主题名称
				throw new Exception(Lang::get('template "{:tpl}" theme not found',['tpl'=>$_tpl]));
			}
			$tpl=substr($tpl,$pos+1);
		}
		elseif($this->config['theme']) // 是否设置了主题
		{
			$theme=$this->config['theme'];
		}
		
		if(strpos($tpl,'/') === false and $this->route['module']){
			$modName = $this->route['module'];
		}
		
		// 检测主题目录文件
		if($theme){
			if(is_file($path=File::path($dir,$theme,$tpl))){
				return $path;
			}
			if(is_file($path=File::path($this->config['themes_path'],$theme,$tpl))){
				return $path;
			}
			if($modName and is_file($path=File::path($dir,$theme,$modName,$tpl))){
				return $path;
			}
			if($modName and is_file($path=File::path($this->config['themes_path'],$theme,$modName,$tpl))){
				return $path;
			}
		}
		
		// 检测已经指定目录的文件
		if(is_file($path=File::path($dir,$tpl))){
			return $path;
		}
		if($modName and is_file($path=File::path($dir,$modName,$tpl))){
			return $path;
		}
		
		// 最后检测框架内模板文件 把扩展名修改为 html
		$tpl = File::addExt($_tpl,'html');
		if(is_file($path=File::path(XG_PHP,'view',$tpl))){
			return $path;
		}
	}
	// 将模板文件转换成php文件
	public function phpFile($name){
		// 文件缓存路径
		$cachePath=$this->cachePath($name);
		// 模板文件路径
		$tplPath=$this->tplPath($name);
		if(!$tplPath){
			throw new Exception(Lang::get('template "{:tpl}" not found',['tpl'=>$name]));
		}

		// phpFile(如果开启了缓存 检测缓存文件是否过期
		if(!XG_DEBUG and $this->config['cache'] and is_file($cachePath)) {
			$tplTime=filemtime($tplPath);
			$phpTime=filemtime($cachePath);
			// 模板文件没有更新
			if($phpTime >= $tplTime) {
				return $cachePath;
			}
		}
		
		// 解析模板内容
		$tplCont=$this->parse(File::cont($tplPath));
		
		// 保存模板内容
		$saved=$this->saveCache($cachePath,$tplCont);
		
		if(!$saved){
			// 没有保存成功 报错
			throw new Exception(Lang::get('template cache "{:path}" save failed',['path'=>$cachePath]));
		}
		return $cachePath;
	}
	// 转换变量名称 $a.b.c 转换成 $a['b']['c']
	protected function convProp($str){
		if(!preg_match('/(^| |\$)([a-zA-Z0-9_\-]+)\.([a-zA-Z0-9_\-]+)/',$str)){
			if(strpos($str,'(')!==false)return $str;
			return (strpos($str,'$')===0?'':'$').$str;
		}
		$str=preg_replace_callback('/\.([a-zA-Z0-9_\-]+)/',function($matches){
			return '['.(is_numeric($matches[1])?'':'"').trim($matches[1],'"\'').(is_numeric($matches[1])?'':'"').']';
		},$str);
		return (strpos($str,'$')===0?'':'$').$str;
	}
	
	// 解析模板扩展
	public function parseExtend($tplCont){
		// 正则规则为 {extend name="tplName" /}
		if(preg_match('/\{extend name=\"(.+?)\"\s*\/?\}/i',$tplCont,$matches)){
			// 解析扩展目标文件模板内容
			if(!$tplPath=$this->tplPath($matches[1])){
				throw new Exception(Lang::get('template "{:tpl}" not found',['tpl'=>$matches[1]]));
			}
			$extendCont=$this->parse(File::cont($tplPath));

			// 删除导入标签
			$extendCont=preg_replace('/\{extend name=\"(.+?)\"\s*\/?\}/is','',$extendCont);

			// 获取当前模板文件解析后的区块内容
			$blocks=[];
			// 正则规则为 {block name="blockName"}<!-- tpl content -->{/block}
			if(preg_match_all('/\{block name=\"(.+?)\"\}(.*?)\{\/block\}/is',$tplCont,$matches)){
				for($i=0;$i<count($matches[0]);$i++){
					$blockName=$matches[1][$i];
					$blocks[$blockName]=$this->parse($matches[2][$i]);
				}
			}
			// 替换带有默认内容的区块
			$extendCont=preg_replace_callback('/\{block name=\"(?<name>.+?)\"\}(?<content>.*?)\{\/block\}/is',function($matches)use($blocks){
				// 如果当前模板内容存在此区块内容
				if($blocks[$matches['name']]){
					$block=$blocks[$matches['name']];
					unset($blocks[$matches['name']]);
					return $block;
				}
				// 如果此区块有默认内容 解析默认内容
				if($matches['content']){
					return $matches['content'];
				}
				return '';
			},$extendCont);

			// 替换扩展目标模板区块内容 正则规则为 {block name="blockName" /} 或者是 {block name="blockName"}<!-- default tpl content -->{/block}
			$extendCont=preg_replace_callback('/\{block name=\"(?<name>.+?)\"\s*\/?\}/is',function($matches)use($blocks){
				// 如果当前模板内容存在此区块内容
				if($blocks[$matches['name']]){
					return $blocks[$matches['name']];
				}
				return '';
			},$extendCont);

			return $extendCont;
		}
	}
	// 解析模板内容
	public function parse($tplCont){
		// 运行优先解析钩子
		$tplCont = Hook::create('view.parse-before')->default($tplCont)->run($tplCont)->last();

		// 匹配到模板扩展 直接返回模板扩展解析结果
		if($extendCont=$this->parseExtend($tplCont)){
			return $extendCont;
		}
		
		$ignoreConts=[];

		// 不解析的内容 {ignore}<!-- tpl content -->{/ignore}
		$tplCont=preg_replace_callback('/\{ignore}(.*?)\{\/ignore\}/is',function($matches) use(&$ignoreConts){
			$rand = rand(99999999,999999999);
			$name="<!-- tpl-ignore-content-{$rand} -->";
			$ignoreConts[$name]=$matches[1];
			return $name;
		},$tplCont);
		
		// 解析指定的内容并保存到变量 {html var="varName"}<!-- tpl content -->{/html}
		$tplCont=preg_replace_callback('/\{html var=\"(.+?)\"}(.*?)\{\/html\}/is',function($matches){
			$varName=$matches[1];
			if(substr($varName,0,1)!=='$'){
				$varName='$'.$varName;
			}
			$tplCont='<?php ob_start();?>'.$matches[2].'<?php '.$varName.'=ob_get_contents();ob_end_clean();?>';
			return $tplCont;
		},$tplCont);
		
		// 解析表单标签
		$tplCont=preg_replace_callback('/\{input ([^}]+)\}/is',function($matches){
			if(preg_match_all('/([a-z]+)=\"([^\"]+)\"/',$matches[1],$pMatches)){
				$params=[];
				for($i=0;$i<count($pMatches[1]);$i++){
					$params[$pMatches[1][$i]]=$pMatches[2][$i];
				}
				$_arr = [];
				foreach($params as $key=>$val){
					if(substr($val,0,1)==':') {
						$_arr[]='"'.$key.'"=>'.substr($val,1);
					} else {
						$_arr[]='"'.$key.'"=>"'.$val.'"';
					}
				}
				$options = '['.Arr::join($_arr,',').']';
				$tplCont = '<?php echo \xg\Form::create('.$options.');?>';
				return $tplCont;
			}
		},$tplCont);

		// 解析指定的内容并保存到变量 {html var="varName"}<!-- tpl content -->{/html}
		$tplCont=preg_replace_callback('/\{html var=\"(.+?)\"}(.*?)\{\/html\}/is',function($matches){
			$varName=$matches[1];
			if(substr($varName,0,1)!=='$'){
				$varName='$'.$varName;
			}
			$tplCont='<?php ob_start();?>'.$matches[2].'<?php '.$varName.'=ob_get_contents();ob_end_clean();?>';
			return $tplCont;
		},$tplCont);

		// 定义变量 {var name="name" value="value" /}
		$tplCont=preg_replace_callback('/\{var (.*?)\/\}/is',function($matches){
			if(preg_match_all('/([a-z]+)=\"([^\"]+)\"/',$matches[1],$pMatches)){
				$params=[];
				for($i=0;$i<count($pMatches[1]);$i++){
					$params[$pMatches[1][$i]]=$pMatches[2][$i];
				}
				if($params['name'] and $params['action']){
					$name=$this->convProp($params['name']);
					$value=$this->convProp($params['action']);
					$tplCont='<?php '.$name.'='.$value.';?>';
				}
			}
			return $tplCont;
		},$tplCont);
		
		// 解析模板变量标签
		$tplCont=preg_replace_callback('/\{\$([a-zA-Z0-9\._\[\]\$\?\:\'\"\-]+)(?:\|([^\}]+))?\}/',function($matches){
			$varStr=$matches[1];
			$defVal=$matches[2];

			// 默认值
			if($defVal){
				$defVal=ltrim($defVal,'|');
			}

			if(substr($defVal,0,1)=='$') // 如果默认值是变量 将模板形式变量转换为php形式变量
			{
				$defVal=$this->convProp($defVal);
			}
			// 如果默认值 没有加引号 并且不是数字 也不是常量 那么加上引号
			elseif($defVal and !preg_match('/^[\'"].+?[\'"]$/',$defVal) and !is_numeric($defVal) and !defined($defVal))
			{
				$defVal='"'.$defVal.'"';
			}

			// 将模板形式变量转换为php形式变量
			$varStr=$this->convProp($varStr);

			if($defVal){
				return '<?php echo (is_null('.$varStr.')?'.$defVal.':'.$varStr.');?>';
			}else{
				return '<?php echo '.$varStr.';?>';
			}
		},$tplCont);

		// 解析模板函数标签 {:functionName(param1,param2)}
		$tplCont=preg_replace_callback('/\{\:([a-zA-Z0-9\._\[\]\$]+)\((.*?)\)[\;]*\}/',function($matches){
			return '<?php echo '.$matches[1].'('.$matches[2].');?>';
		},$tplCont);

		// 解析模板包含标签 {include name="tplName" /}
		$tplCont=preg_replace_callback('/\{include name=\"(?<name>.+?)\"(?: try=\"(?<try>.+?)\")?\}(?:(?<content>.*?)\{\/block\})?/is',function($matches){
			return '<?php require \xg\facade\View::phpFile("'.$matches['name'].'");?>';
		},$tplCont);

		// 解析模板尝试包含标签 {try name="tplNames" /}
		$tplCont=preg_replace_callback('/\{tpl try=\"(?<try>.+?)\"\}(?:(?<content>.*?)\{\/tpl\})?/is',function($matches){
			$tpls = Arr::split($matches['try'],'|');
			return '<?php
				$tpls = ["'.Arr::join($tpls,'","').'"];
				$tryTplExists = false;
				foreach($tpls as $tpl){
					if(\xg\facade\View::tplPath($tpl)){
						$tryTplExists = true;
						require \xg\facade\View::phpFile($tpl);
					}
				}
				if(!$tryTplExists){ ?>'.$matches['content'].'<?php }
			?>';
		},$tplCont);

		// 处理 if 标签
		$tplCont=preg_replace_callback('/\{(if)[\s]+(.*?)}/is',function($matches){
			if(strpos($matches[2],'.')>0)$matches[2]=$this->convProp($matches[2]);
			return '<?php '.$matches[1].'('.$matches[2].'){?>';
		},$tplCont);

		// 处理 for 标签
		$tplCont=preg_replace_callback('/\{(for)[\s]+(.*?)}/is',function($matches){
			if(strpos($matches[2],'.')>0)$matches[2]=$this->convProp($matches[2]);
			return '<?php '.$matches[1].'('.$matches[2].'){?>';
		},$tplCont);

		// 处理 elseif 标签
		$tplCont=preg_replace_callback('/\{(else if|elseif)[\s]+(.*?)}/is',function($matches){
			if(strpos($matches[2],'.')>0)$matches[2]=$this->convProp($matches[2]);
			return '<?php }'.$matches[1].'('.$matches[2].'){?>';
		},$tplCont);

		// 处理 else 标签
		$tplCont=preg_replace_callback('/\{else}/is',function($matches){
			return '<?php }else{?>';
		},$tplCont);

		// 处理 foreach 标签
		$tplCont=preg_replace_callback('/\{foreach ([\$]?[a-zA-Z0-9\._\[\]\$\'\"\(\)]+)[\s]+([\$]?[a-zA-Z0-9_]+)([\s]+([\$]?[a-zA-Z0-9_]+))*}/is',function($matches){
			if(strpos($matches[1],'.')>0)$matches[1]=$this->convProp($matches[1]);
			if($matches[4]){
				$dataName=$matches[1];
				$keyName=$matches[2];
				$valueName=$matches[4];
				if(substr($dataName,0,1)!='$' and strpos($dataName,'(')===false){
					$dataName='$'.$dataName;
				}
				if(substr($keyName,0,1)!='$'){
					$keyName='$'.$keyName;
				}
				if(substr($valueName,0,1)!='$'){
					$valueName='$'.$valueName;
				}
				return '<?php foreach('.$dataName.' as '.$keyName.'=>'.$valueName.'){?>';
			}else{
				$dataName=$matches[1];
				$valueName=$matches[2];
				if(substr($dataName,0,1)!='$' and strpos($dataName,'(')===false){
					$dataName='$'.$dataName;
				}
				if(substr($valueName,0,1)!='$'){
					$valueName='$'.$valueName;
				}
				return '<?php foreach('.$dataName.' as '.$valueName.'){?>';
			}
		},$tplCont);

		// 处理 if for foreach 闭合标签
		$tplCont=preg_replace_callback('/\{\/(if|foreach|for)\}/is',function($matches){
			return '<?php };?>';
		},$tplCont);

		// 处理 echo 标签
		$tplCont=preg_replace_callback('/\{echo(.*?)\/\}/is',function($matches){
			$var=$matches[1];
			if(substr($var,0,1)=='$'){
				// 如果是变量 将模板形式变量转换为php形式变量
				$var=$this->convProp($var);
			}elseif(!preg_match('/^[\'"].+?[\'"]$/',$var) and !is_numeric($var) and !defined($var)){
				// 如果没有加引号 并且不是数字 也不是常量 那么加上引号
				$var='"'.$var.'"';
			}
			return '<?php echo '.$var.';?>';
		},$tplCont);

		// 处理 php 标签 {php $phpcode=phpcode(); /}
		$tplCont=preg_replace_callback('/\{php(.*?)\/\}/is',function($matches){
			return '<?php '.trim($matches[1],'; 	').';?>';
		},$tplCont);

		// 运行后解析钩子
		$tplCont = Hook::create('view.parse-after')->default($tplCont)->run($tplCont)->last();
		
		foreach($ignoreConts as $name=>$cont){
			$tplCont=str_replace($name,$cont,$tplCont);
		}
		
		return $tplCont;
	}

	// 获取文件缓存路径
	protected function cachePath($name){
		$path=File::path($this->tplPath($name));
		if(!$path){
			throw new Exception(Lang::get('template "{:tpl}" not found',['tpl'=>$name]));
		}
		
		// 为了保持缓存文件名称的一致 删除路径中的项目根目录部分
		if(strpos($path,XG_ROOT)===0){
			$path=substr($path,strlen(XG_ROOT));
		}
		// 根据路径生成唯一文件名称
		$hashName=substr(md5($path),0,16).'.php';
		// 返回文件存储路径
		return File::path($this->config['temp'],$hashName);
	}
	
	// 保存模板缓存文件
	protected function saveCache($path,$cont){
		// 创建报错文件的上层目录
		mkdir(dirname($path),0755,true);
		// 添加限制访问代码
		$head='<?php if(!defined("XG"))exit("Access denied!");?>';
		if(strpos($cont,$head)!==0){
			$cont=$head.$cont;
		}
		// 保存文件
		return File::saveCont($path,$cont);
	}
	// 向模板传值
	public function assign($name,$value=null){
		if(is_array($name)){
			// 合并数据
			$this->data=Arr::extend($this->data,$name);
		}elseif(strpos($name,'.')>0){
			// 多级赋值
			$names=Arr::split($name,'.');
			$tree=Arr::get($names,$value);
			$this->data=Arr::extend($this->data,$tree,true);
		}else{
			// 修改数据
			$this->data[$name]=$value;
		}
		return $this;
	}
}